Skip to content

⚡ Bolt: Optimize high-frequency engine simulation hot paths#14

Open
lonewolf0708 wants to merge 1 commit intolonewolf0708-patch-10-floodingfrom
bolt/optimize-engine-hotpaths-7832147704157988298
Open

⚡ Bolt: Optimize high-frequency engine simulation hot paths#14
lonewolf0708 wants to merge 1 commit intolonewolf0708-patch-10-floodingfrom
bolt/optimize-engine-hotpaths-7832147704157988298

Conversation

@lonewolf0708
Copy link
Copy Markdown
Owner

@lonewolf0708 lonewolf0708 commented Apr 20, 2026

💡 What:

  • Moved enrichmentMap and getColdEnrichment to module scope to avoid redundant allocations in updateTorque.
  • Refactored initBattery to run once during initialization instead of every frame in updateGFX.
  • Renamed shadowed dt variable in updateGFX to batteryDt.

🎯 Why:

  • updateTorque runs at ~2000Hz. Re-allocating a function and table literal every cycle causes significant GC pressure and CPU overhead.
  • updateGFX runs at ~60Hz. Re-defining functions and checking initialization state every frame is inefficient.
  • Shadowing dt is a dangerous pattern that can lead to bugs in time-based logic.

📊 Impact:

  • Significant reduction in garbage collection pressure during engine simulation.
  • Slight reduction in per-frame CPU usage in updateGFX.
  • Improved code safety and maintainability.

🔬 Measurement:

  • Manual code review confirms the removal of allocations from hot paths.

PR created automatically by Jules for task 7832147704157988298 started by @lonewolf0708

Summary by CodeRabbit

  • Documentation

    • Added performance optimization learning notes for developers.
  • Refactor

    • Optimized internal engine logic to improve performance and reduce potential stuttering.

- Hoist enrichmentMap and getColdEnrichment to module scope to reduce GC pressure in updateTorque (~2000Hz).
- Move initBattery to engine initialization to avoid redundant work in updateGFX (~60Hz).
- Fix variable shadowing of dt in updateGFX.

Co-authored-by: lonewolf0708 <51030924+lonewolf0708@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 20, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1fffd8ee-e63f-4848-b254-4a5a2575b02b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bolt/optimize-engine-hotpaths-7832147704157988298

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
combustionEngine.lua (2)

1281-1294: ⚠️ Potential issue | 🟠 Major

Wire the cold enrichment into the fuel calculation.

Operator, the helper now computes coldStartEnrichment, but the value is discarded—tragic, wasteful! The hot path still pays for the lookup, and cold-start fueling remains unchanged.

🐛 Proposed fix
-  local baseFuelAmount = 0.02  -- Base fuel scaled by enrichment
+  local baseFuelAmount = 0.02 * coldStartEnrichment  -- Base fuel scaled by enrichment

If enrichment is not intended yet, remove the call until it is used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@combustionEngine.lua` around lines 1281 - 1294, coldStartEnrichment is being
computed by getColdEnrichment(engineTempC) but never used; update the fuel
calculation in the cranking/hot path to incorporate it (or remove the call).
Specifically, in the block that computes final fuel for cranking/idle (use
symbols baseFuelAmount, crankingFuelMultiplier, chokeEffect, chokeThrottleBoost
and isCranking/engineTempC to find the correct spot), apply the enrichment by
multiplying or adding coldStartEnrichment into the fuel amount (e.g., scale
baseFuelAmount or apply to crankingFuelMultiplier) so cold starts get richer
fuel; alternatively remove the unused getColdEnrichment call if enrichment is
intentionally deferred.

647-666: ⚠️ Potential issue | 🟠 Major

Use the real frame delta for battery charge and drain.

Renaming the shadowed value is good, but batteryDt = 1/60 still makes battery state depend on how often updateGFX actually runs. Frame drops, slow motion, or a non-60Hz update cadence will skew drain/recharge.

🐛 Proposed fix
-  local batteryDt = 1/60  -- Fixed timestep for battery updates
+  local batteryDt = dt  -- Use the actual update interval for battery updates
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@combustionEngine.lua` around lines 647 - 666, The battery update uses a
hardcoded batteryDt = 1/60 which makes charge/drain wrong when the update
cadence changes; replace that constant with the real frame delta provided by the
update loop (e.g., the dt/deltaTime parameter passed into updateGFX or the
engine update function) and use it (in seconds) in the battery math where
batteryDt is referenced; update logic around device.batteryCharge,
device.batteryDrainRate, device.batteryChargeRate, device.outputAV1,
device.idleAV, device.starterEngagedCoef and device.starterMaxAV to use the
actual dt (converting units if the loop supplies milliseconds) so
drainRate/chargeRate are scaled correctly by elapsed time instead of a fixed
1/60.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@combustionEngine.lua`:
- Around line 39-81: The interpolation sentinels in getColdEnrichment are wrong:
lowerTemp/upperTemp and their enrich defaults don't reflect actual enrichmentMap
entries (causing -20..-10 to interpolate against wrong values and using a hidden
80→0.85 point). Fix by either (A) making the 80→0.85 point explicit in
enrichmentMap, or (B) change getColdEnrichment to initialize lowerTemp/upperTemp
and lowerEnrich/upperEnrich from the actual first/last keys of enrichmentMap (or
use -math.huge/+math.huge sentinels) and then pick the nearest lower/upper
entries during the pairs() scan so the enrich values always come from
enrichmentMap entries (use enrichmentMap[temp] to set lowerEnrich/upperEnrich
when updating lowerTemp/upperTemp).

---

Outside diff comments:
In `@combustionEngine.lua`:
- Around line 1281-1294: coldStartEnrichment is being computed by
getColdEnrichment(engineTempC) but never used; update the fuel calculation in
the cranking/hot path to incorporate it (or remove the call). Specifically, in
the block that computes final fuel for cranking/idle (use symbols
baseFuelAmount, crankingFuelMultiplier, chokeEffect, chokeThrottleBoost and
isCranking/engineTempC to find the correct spot), apply the enrichment by
multiplying or adding coldStartEnrichment into the fuel amount (e.g., scale
baseFuelAmount or apply to crankingFuelMultiplier) so cold starts get richer
fuel; alternatively remove the unused getColdEnrichment call if enrichment is
intentionally deferred.
- Around line 647-666: The battery update uses a hardcoded batteryDt = 1/60
which makes charge/drain wrong when the update cadence changes; replace that
constant with the real frame delta provided by the update loop (e.g., the
dt/deltaTime parameter passed into updateGFX or the engine update function) and
use it (in seconds) in the battery math where batteryDt is referenced; update
logic around device.batteryCharge, device.batteryDrainRate,
device.batteryChargeRate, device.outputAV1, device.idleAV,
device.starterEngagedCoef and device.starterMaxAV to use the actual dt
(converting units if the loop supplies milliseconds) so drainRate/chargeRate are
scaled correctly by elapsed time instead of a fixed 1/60.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 717989f7-fc3c-4c45-bbf8-79d6f5a07287

📥 Commits

Reviewing files that changed from the base of the PR and between 3aff9bb and ce10b18.

📒 Files selected for processing (2)
  • .jules/bolt.md
  • combustionEngine.lua

Comment thread combustionEngine.lua
Comment on lines +39 to +81
-- Static enrichment map to avoid re-allocation in the hot path
local enrichmentMap = {
[-30] = 3.0,
[-20] = 2.6,
[-10] = 2.2,
[0] = 1.8,
[10] = 1.5,
[20] = 1.3,
[30] = 1.15,
[40] = 1.05,
[50] = 1.02,
[60] = 1.0,
[70] = 1.0
}

-- Optimized cold start enrichment calculation moved to module scope
local function getColdEnrichment(tempC)
-- Find the two closest temperature points
local lowerTemp = -20
local upperTemp = 80
local lowerEnrich = 3.0
local upperEnrich = 0.85

-- Find the two closest temperature points in the map
for temp, enrich in pairs(enrichmentMap) do
if temp <= tempC and temp > lowerTemp then
lowerTemp = temp
lowerEnrich = enrich
end
if temp >= tempC and temp < upperTemp then
upperTemp = temp
upperEnrich = enrich
end
end

-- Linear interpolation between the two closest points
if lowerTemp == upperTemp then
return lowerEnrich
end

local t = (tempC - lowerTemp) / (upperTemp - lowerTemp)
return lowerEnrich + (upperEnrich - lowerEnrich) * t
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix the cold-enrichment interpolation bounds.

Operator, the sentinels are... confused. lowerTemp = -20 starts with lowerEnrich = 3.0, so temperatures between -20 and -10 interpolate from the wrong enrichment. Values above 70 also use an implicit 80 → 0.85 point that is not in the map.

🐛 Proposed fix
--- Static enrichment map to avoid re-allocation in the hot path
-local enrichmentMap = {
-  [-30] = 3.0,
-  [-20] = 2.6,
-  [-10] = 2.2,
-  [0] = 1.8,
-  [10] = 1.5,
-  [20] = 1.3,
-  [30] = 1.15,
-  [40] = 1.05,
-  [50] = 1.02,
-  [60] = 1.0,
-  [70] = 1.0
+-- Static enrichment points to avoid re-allocation in the hot path
+local enrichmentPoints = {
+  {-30, 3.0},
+  {-20, 2.6},
+  {-10, 2.2},
+  {0, 1.8},
+  {10, 1.5},
+  {20, 1.3},
+  {30, 1.15},
+  {40, 1.05},
+  {50, 1.02},
+  {60, 1.0},
+  {70, 1.0}
 }
 
 -- Optimized cold start enrichment calculation moved to module scope
 local function getColdEnrichment(tempC)
-  -- Find the two closest temperature points
-  local lowerTemp = -20
-  local upperTemp = 80
-  local lowerEnrich = 3.0
-  local upperEnrich = 0.85
-
-  -- Find the two closest temperature points in the map
-  for temp, enrich in pairs(enrichmentMap) do
-    if temp <= tempC and temp > lowerTemp then
-      lowerTemp = temp
-      lowerEnrich = enrich
-    end
-    if temp >= tempC and temp < upperTemp then
-      upperTemp = temp
-      upperEnrich = enrich
-    end
+  if tempC <= enrichmentPoints[1][1] then
+    return enrichmentPoints[1][2]
   end
 
-  -- Linear interpolation between the two closest points
-  if lowerTemp == upperTemp then
-    return lowerEnrich
+  for i = 2, `#enrichmentPoints` do
+    local upperPoint = enrichmentPoints[i]
+    if tempC <= upperPoint[1] then
+      local lowerPoint = enrichmentPoints[i - 1]
+      local t = (tempC - lowerPoint[1]) / (upperPoint[1] - lowerPoint[1])
+      return lowerPoint[2] + (upperPoint[2] - lowerPoint[2]) * t
+    end
   end
 
-  local t = (tempC - lowerTemp) / (upperTemp - lowerTemp)
-  return lowerEnrich + (upperEnrich - lowerEnrich) * t
+  return enrichmentPoints[`#enrichmentPoints`][2]
 end

If the 80°C → 0.85 behavior is intentional, make it an explicit point instead of a hidden default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@combustionEngine.lua` around lines 39 - 81, The interpolation sentinels in
getColdEnrichment are wrong: lowerTemp/upperTemp and their enrich defaults don't
reflect actual enrichmentMap entries (causing -20..-10 to interpolate against
wrong values and using a hidden 80→0.85 point). Fix by either (A) making the
80→0.85 point explicit in enrichmentMap, or (B) change getColdEnrichment to
initialize lowerTemp/upperTemp and lowerEnrich/upperEnrich from the actual
first/last keys of enrichmentMap (or use -math.huge/+math.huge sentinels) and
then pick the nearest lower/upper entries during the pairs() scan so the enrich
values always come from enrichmentMap entries (use enrichmentMap[temp] to set
lowerEnrich/upperEnrich when updating lowerTemp/upperTemp).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant